﻿// -----------------------------------------------------------------------
// <copyright file="MethodInvokerRunner.cs" company="Microsoft">
//      Copyright (c) Microsoft Corporation.  All rights reserved.
// </copyright>
// -----------------------------------------------------------------------

namespace GenericMethodInvoker
{
    using System;
    using System.Collections.Generic;
    using System.Configuration;
    using System.Diagnostics;
    using System.Globalization;
    using System.Linq;
    using System.Reflection;
    using System.Threading;
    using XmlConfigReader;

    /// <summary>
    /// Objects that invokes the Methods for the supplied Scenarios
    /// </summary>
    public class MethodInvokerRunner
    {
        /// <summary>
        /// Fetches the scenarios with the supplied names from Config and runs them.
        /// </summary>
        /// <param name="scenarios">The scenarios.</param>
        public static void RunScenarios(string[] scenarios)
        {
            var scenarioConfigs = ConfigReader.Scenarios;

            if (scenarios.Count() == 0)
            {
                Console.WriteLine("No Scenarios run. Please supply the space seperated scenario names to run.");
                return;
            }

            if (string.Compare(scenarios[0], "all", StringComparison.OrdinalIgnoreCase) != 0)
            {
                scenarioConfigs = scenarioConfigs.Where(scenario => scenarios.Any(givenScenario => string.Compare(scenario.Name, givenScenario, StringComparison.OrdinalIgnoreCase) == 0)).ToList();
            }

            if (scenarioConfigs.Count == 0)
            {
                Console.WriteLine("No Scenario matched the supplied names.");
                return;
            }

            foreach (var scenarioConfig in scenarioConfigs.Where(p => p.Include == true))
            {
                var exceptions = new List<Exception>();
                foreach (var objectInstanceConfig in scenarioConfig.ObjectInstances)
                {
                    try
                    {
                        CreateObjectInstance(objectInstanceConfig);
                    }
                    catch (Exception exception)
                    {
                        exceptions.Add(exception);
                    }
                }

                if (exceptions.Count < 1)
                {
                    continue;
                }

                Console.Error.WriteLine(string.Format("Scenario {0} completed with Errors.", scenarioConfig.Name));
                foreach (var exception in exceptions)
                {
                    Console.Error.WriteLine(exception.Message);
                }
            }
        }

        /// <summary>
        /// Invokes the method.
        /// </summary>
        /// <param name="concreteObject">The concrete object.</param>
        /// <param name="concreteObjectType">Type of the concrete object.</param>
        /// <param name="methodName">Name of the method.</param>
        /// <param name="paramObjects">The parameter objects.</param>
        private static void InvokeMethod(object concreteObject, Type concreteObjectType, string methodName, object[] paramObjects)
        {
            var methodInfo = concreteObjectType.GetMethod(methodName);
            if (methodInfo != null)
            {
                Console.WriteLine(string.Format("Calling Method {0}()", methodInfo.Name));
                var watch = new Stopwatch();
                watch.Start();
                methodInfo.Invoke(concreteObject, BindingFlags.Public, null, paramObjects, CultureInfo.InvariantCulture);
                watch.Stop();
                Console.WriteLine(string.Format("Time taken: {0} milliseconds.", watch.ElapsedMilliseconds));
            }
            else
            {
                Console.WriteLine(
                    string.Format(
                        "No public method with the name '{0}' found in '{1}' type", methodName, concreteObjectType.Name));
            }
        }

        /// <summary>
        /// Creates the object instance.
        /// </summary>
        /// <param name="objectInstanceConfig">The object instance config.</param>
        /// <returns>returns an object of dynamic type.</returns>
        private static dynamic CreateObjectInstance(ObjectInstance objectInstanceConfig)
        {
            dynamic concreteObject = null;
            var concreteObjectType = Type.GetType(objectInstanceConfig.ObjectType);

            if (concreteObjectType == null && !string.IsNullOrEmpty(objectInstanceConfig.ObjectAssembly))
            {
                var assembly = Assembly.LoadFile(string.Format("{0}\\{1}", Environment.CurrentDirectory, objectInstanceConfig.ObjectAssembly));
                concreteObjectType = assembly.GetType(objectInstanceConfig.ObjectType);
            }

            if (concreteObjectType != null)
            {
                if (objectInstanceConfig.InitializeThroughConstructor)
                {
                    concreteObject = InvokeObjectConstructor(concreteObjectType, objectInstanceConfig.ConstructorParams);
                }
                else
                {
                    if (!concreteObjectType.IsPrimitive && concreteObjectType != typeof(string) && !objectInstanceConfig.IsStatic)
                    {
                        concreteObject = Activator.CreateInstance(concreteObjectType, true);
                    }

                    if (!string.IsNullOrEmpty(objectInstanceConfig.ObjectValue))
                    {
                        concreteObject = Convert.ChangeType(objectInstanceConfig.ObjectValue, concreteObjectType);
                    }
                }

                if (concreteObject != null || objectInstanceConfig.IsStatic)
                {
                    SetProperties(concreteObject, concreteObjectType, objectInstanceConfig.Properties);
                    RunMethodsOnConcreteObject(concreteObject, concreteObjectType, objectInstanceConfig.MethodCalls);
                }
            }
            else
            {
                throw new ConfigurationErrorsException(string.Format("Error resolving object type {0}. Invalid Type {1}", objectInstanceConfig.ObjectName, objectInstanceConfig.ObjectType));
            }

            return concreteObject;
        }

        /// <summary>
        /// Runs the methods on concrete object.
        /// </summary>
        /// <param name="concreteObject">The concrete object.</param>
        /// <param name="concreteObjectType">Type of the concrete object.</param>
        /// <param name="methodCalls">The method calls.</param>
        private static void RunMethodsOnConcreteObject(dynamic concreteObject, Type concreteObjectType, IEnumerable<MethodCall> methodCalls)
        {
            foreach (var methodCall in methodCalls)
            {
                if (!methodCall.Include)
                    {
                        continue;
                    }

                foreach (var iteration in methodCall.Iterations)
                {
                    int repeat = iteration.IterationRepeatCount;
                    Thread.CurrentThread.CurrentCulture = !string.IsNullOrEmpty(iteration.IterationCultureName) 
                                                            ? CultureInfo.GetCultureInfo(iteration.IterationCultureName) 
                                                            : CultureInfo.InvariantCulture;

                    while (repeat-- > 0)
                    {
                        InvokeMethod(
                            concreteObject, 
                            concreteObjectType, 
                            methodCall.MethodName, 
                            iteration.Parameters.Select(CreateObjectInstance).Cast<object>().ToArray());
                    }
                }
            }
        }

        /// <summary>
        /// Sets the properties.
        /// </summary>
        /// <param name="concreteObject">The concrete object.</param>
        /// <param name="concreteObjectType">Type of the concrete object.</param>
        /// <param name="properties">The properties.</param>
        private static void SetProperties(dynamic concreteObject, Type concreteObjectType, IEnumerable<ObjectInstance> properties)
        {
            foreach (var property in properties)
            {
                var propertyObject = CreateObjectInstance(property);
                var propertyInfo = concreteObjectType.GetProperty(property.ObjectName);
                propertyInfo.SetValue(concreteObject, propertyObject, null);
            }
        }

        /// <summary>
        /// Invokes the object constructor.
        /// </summary>
        /// <param name="objectType">Type of the object.</param>
        /// <param name="constructorParams">The constructor params.</param>
        /// <returns>Constructed Object</returns>
        private static dynamic InvokeObjectConstructor(Type objectType, IEnumerable<ObjectInstance> constructorParams)
        {
            var ctrParameters = constructorParams.Select(CreateObjectInstance).Cast<object>().ToList();

            var constructorInfo = objectType.GetConstructor(ctrParameters.ToList().Select(p => p.GetType()).ToArray());
            if (constructorInfo != null)
            {
                return constructorInfo.Invoke(ctrParameters.ToArray());
            }

            throw new ApplicationException(string.Format("Cannot find matching Constructor with supplied set of parameters for type: {0}", objectType.ToString()));
        }
    }
}
